﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;
using System.IO;

namespace Kokomo.Mirror.CliMetadata
{
	class MetadataRowReaderBuilder
	{
		public MetadataRowReaderBuilder(
			MetadataReader reader,
			MetadataContainer container,
			Type rowType
			)
		{
			if (reader == null) throw new ArgumentNullException("reader");
			if (container == null) throw new ArgumentNullException("container");
			if ((object)rowType == null) throw new ArgumentNullException("rowType");

			this.Container = container;
			this.RowType = rowType;
			this.BuildMethodStub();
			this.Reader = reader;
		}

		private void BuildMethodStub()
		{
			DynamicMethod readerMethod = new DynamicMethod("reader", typeof(void), new Type[] { typeof(MetadataReader), typeof(IMetadataRow) }, typeof(MetadataReader), true);
			this._readerMethod = readerMethod;
			readerMethod.DefineParameter(1, ParameterAttributes.None, null);
			readerMethod.DefineParameter(2, ParameterAttributes.None, null);

			ILGenerator ilgen = readerMethod.GetILGenerator();
			this._ilgen = ilgen;
			LocalBuilder readerLocal = ilgen.DeclareLocal(typeof(MetadataReader));
			this._readerLocal = readerLocal;
			LocalBuilder rowLocal = ilgen.DeclareLocal(this.RowType);
			this._rowLocal = rowLocal;

			ilgen.Emit(OpCodes.Ldarg_0);
			ilgen.Emit(OpCodes.Stloc, readerLocal);
			ilgen.Emit(OpCodes.Ldarg_1);
			ilgen.Emit(OpCodes.Unbox_Any, this.RowType);
			ilgen.Emit(OpCodes.Stloc, rowLocal);
		}

		public MetadataReader Reader { get; private set; }
		public MetadataContainer Container { get; private set; }
		public Type RowType { get; private set; }
		private DynamicMethod _readerMethod;
		private ILGenerator _ilgen;
		private LocalBuilder _rowLocal;
		private LocalBuilder _readerLocal;

		public Action<MetadataReader, IMetadataRow> GetDelegate()
		{
			this._ilgen.Emit(OpCodes.Ret);

			return (Action<MetadataReader, IMetadataRow>)this._readerMethod.CreateDelegate(typeof(Action<MetadataReader, IMetadataRow>));
		}

		public void EmitReadProperty(PropertyInfo property)
		{
			if (property == null) throw new ArgumentNullException("property");

			Type fieldType = property.PropertyType;
			if (fieldType == typeof(StringRef))
				this.EmitReadStringRef(property);
			else if (fieldType == typeof(BlobRef))
				this.EmitReadBlobRef(property);
			else if (fieldType == typeof(GuidRef))
				this.EmitReadGuidRef(property);
			else if (fieldType == typeof(CodedIndex))
				this.EmitReadCodedIndex(property);
			else if (fieldType.IsEnum)
				this.EmitReadEnum(property);
			else if (fieldType == typeof(SignatureRef))
			{
				SignatureFieldAttribute signatureFieldAttribute = property.GetCustomAttribute<SignatureFieldAttribute>();
				if (signatureFieldAttribute == null) throw new ArgumentException("Property must be marked with SignatureFieldAttribute");
				this.EmitReadSignatureRef(property, signatureFieldAttribute.Kind);
			}
			else
				this.EmitReadPrimitive(property, fieldType);
		}

		private static MethodInfo GetPropertySetter(PropertyInfo property)
		{
			if (property == null) throw new ArgumentNullException("property");
			MethodInfo setter = property.GetSetMethod(true);
			if (setter == null) throw new ArgumentException("Property is read-only", "property");

			return setter;
		}

		private void EmitMethodCallPropertySet(
			PropertyInfo property,
			MethodInfo fieldReaderMethod
			)
		{
			if (property == null) throw new ArgumentNullException("property");
			if (fieldReaderMethod == null) throw new ArgumentNullException("fieldReaderMethod");

			MethodInfo setter = GetPropertySetter(property);
			ILGenerator ilgen = this._ilgen;
			if (this.RowType.IsValueType)
				ilgen.Emit(OpCodes.Ldloca, this._rowLocal);
			else
				ilgen.Emit(OpCodes.Ldloc, this._rowLocal);
			ilgen.Emit(OpCodes.Ldloc, this._readerLocal);
			ilgen.Emit(OpCodes.Call, fieldReaderMethod);
			ilgen.Emit(OpCodes.Call, setter);
		}
		private void EmitMethodCallPropertySet(
			PropertyInfo property,
			MethodInfo fieldReaderMethod,
			int readerMethodParam
			)
		{
			if (property == null) throw new ArgumentNullException("property");
			if (fieldReaderMethod == null) throw new ArgumentNullException("fieldReaderMethod");

			MethodInfo setter = GetPropertySetter(property);
			ILGenerator ilgen = this._ilgen;
			if (this.RowType.IsValueType)
				ilgen.Emit(OpCodes.Ldloca, this._rowLocal);
			else
				ilgen.Emit(OpCodes.Ldloc, this._rowLocal);
			ilgen.Emit(OpCodes.Ldloc, this._readerLocal);
			ilgen.Emit(OpCodes.Ldc_I4, readerMethodParam);
			ilgen.Emit(OpCodes.Call, fieldReaderMethod);
			ilgen.Emit(OpCodes.Call, setter);
		}
		public void EmitReadStringRef(PropertyInfo property)
		{
			MethodInfo readerMethod =
				this.Container.HasLargeStringHeap
				? ((Func<StringRef>)this.Reader.ReadLongStringRef).Method
				: ((Func<StringRef>)this.Reader.ReadShortStringRef).Method
				;
			this.EmitMethodCallPropertySet(property, readerMethod);
		}
		public void EmitReadBlobRef(PropertyInfo property)
		{
			MethodInfo readerMethod =
				this.Container.HasLargeBlobHeap
				? ((Func<BlobRef>)this.Reader.ReadLongBlobRef).Method
				: ((Func<BlobRef>)this.Reader.ReadShortBlobRef).Method
				;
			this.EmitMethodCallPropertySet(property, readerMethod);
		}
		public void EmitReadSignatureRef(PropertyInfo property, SignatureKind kind)
		{
			MethodInfo readerMethod =
				this.Container.HasLargeBlobHeap
				? ((Func<SignatureKind, SignatureRef>)this.Reader.ReadLongSignatureRef).Method
				: ((Func<SignatureKind, SignatureRef>)this.Reader.ReadShortSignatureRef).Method
				;
			this.EmitMethodCallPropertySet(property, readerMethod, (int)kind);
		}
		public void EmitReadGuidRef(PropertyInfo property)
		{
			MethodInfo readerMethod =
				this.Container.HasLargeGuidHeap
				? ((Func<MetadataReader, GuidRef>)MetadataReader.ReadLongGuidRef).Method
				: ((Func<MetadataReader, GuidRef>)MetadataReader.ReadShortGuidRef).Method
				;
			this.EmitMethodCallPropertySet(property, readerMethod);
		}
		public void EmitReadCodedIndex(PropertyInfo property)
		{
			SimpleIndexAttribute simpleIndexAttribute = property.GetCustomAttribute<SimpleIndexAttribute>();
			if (simpleIndexAttribute != null)
			{
				int maxRows = this.Container.GetRowCount(simpleIndexAttribute.TableNumber);

				MethodInfo readerMethod =
					(maxRows < 0x10000)
					? ((Func<MetadataReader, MetadataTableNumber, CodedIndex>)MetadataReader.ReadShortIndex).Method
					: ((Func<MetadataReader, MetadataTableNumber, CodedIndex>)MetadataReader.ReadLongIndex).Method
					;

				this.EmitMethodCallPropertySet(property, readerMethod, (int)simpleIndexAttribute.TableNumber);
			}
			else
			{
				CodedIndexAttribute attribute = property.GetCustomAttribute<CodedIndexAttribute>();
				if (attribute == null) throw new InvalidOperationException("Invalid index field");

				CodedIndexType indexType = attribute.Type;

				int selectorBits = CodedIndex.GetSelectorBitCount(indexType);
				int maxTableRows = 0;
				MetadataTableNumber[] tableNumbers = CodedIndex.GetTableNumbers(indexType);
				foreach (var tableNumberOption in tableNumbers)
				{
					if (tableNumberOption != MetadataTableNumber.Invalid)
					{
						int tableRows = this.Container.GetRowCount(tableNumberOption);
						maxTableRows = Math.Max(maxTableRows, tableRows);
					}
				}
				int rowBits = CodedIndex.Log2(maxTableRows);

				MethodInfo readerMethod =
					((rowBits + selectorBits) <= 16)
					? ((Func<MetadataReader, CodedIndexType, CodedIndex>)MetadataReader.ReadShortIndex).Method
					: ((Func<MetadataReader, CodedIndexType, CodedIndex>)MetadataReader.ReadLongIndex).Method
					;

				this.EmitMethodCallPropertySet(property, readerMethod, (int)indexType);
			}
		}
		public void EmitReadEnum(PropertyInfo property)
		{
			ConstantFieldAttribute constantFieldAttribute = property.GetCustomAttribute<ConstantFieldAttribute>();
			Type fieldType = property.PropertyType;

			Type logicalType = fieldType.GetEnumUnderlyingType();
			Type physicalType = (constantFieldAttribute == null)
				? logicalType
				: constantFieldAttribute.FieldType
				;

			this.EmitReadPrimitive(property, physicalType);
		}

		private void EmitReadPrimitive(PropertyInfo property, Type type)
		{
			if ((object)property == null) throw new ArgumentNullException("property");
			if ((object)type == null) throw new ArgumentNullException("type");

			string methodName;
			if (type == typeof(short))
				methodName = "ReadInt16";
			else if (type == typeof(ushort))
				methodName = "ReadUInt16";
			else if (type == typeof(int))
				methodName = "ReadInt32";
			else if (type == typeof(uint))
				methodName = "ReadUInt32";
			else if (type == typeof(byte))
				methodName = "ReadByte";
			else
				throw new InvalidOperationException();

			MethodInfo method = typeof(BinaryReader).GetMethod(methodName);
			MethodInfo setter = property.GetSetMethod(true);

			ILGenerator ilgen = this._ilgen;
			if (this.RowType.IsValueType)
				ilgen.Emit(OpCodes.Ldloca, this._rowLocal);
			else
				ilgen.Emit(OpCodes.Ldloc, this._rowLocal);
			ilgen.Emit(OpCodes.Ldloc, this._readerLocal);
			ilgen.Emit(OpCodes.Call, typeof(MetadataReader).GetProperty("BinaryReader").GetGetMethod(true));
			ilgen.Emit(OpCodes.Call, method);
			ilgen.Emit(OpCodes.Call, setter);
		}
	}
}
